package org.sqlproc.engine.impl;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.hibernate.SQLQuery;
import org.hibernate.type.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The Mapping rule entity for one output attribute.
 * 
 * Instance of this class is created by the ANTLR parser. The grammar itself is defined in SqlMapping.g.
 * 
 * @author <a href="mailto:Vladimir.Hudec@gmail.com">Vladimir Hudec</a>
 */
class SqlMappingItem {

    /**
     * The internal slf4j logger.
     */
    final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * The name of an attribute in the result class. In the case of embedded classes the can be in fact a list of
     * partial names (name1.name2...nameX). This name can be null. In this case the name of the attribute is the same as
     * the name of the query column or alias output.
     */
    private List<String> javaNames;
    /**
     * The name of a database query output - the column name or the alias name.
     */
    private String dbName;
    /**
     * The type of an attribute in the result class and/or the database column type. In fact it's a pair of a Hibernate
     * type and/or an internal type.
     */
    private SqlType sqlType;

    /**
     * Creates a new instance. It's used from inside ANTLR parser.
     * 
     * @param dbName
     *            the name of a database query output - the column name or the alias name
     */
    SqlMappingItem(String dbName) {
        this.javaNames = new ArrayList<String>();
        this.sqlType = new SqlType();
        this.dbName = dbName;
    }

    /**
     * Adds a partial name of an attribute.
     * 
     * @param name
     *            the partial name of an attribute in the result class
     */
    void addName(String name) {
        this.javaNames.add(name);
    }

    /**
     * Assigns an internal type. This type it's used in a conversion process of SQL query output values.
     * 
     * @param sMetaType
     *            String representation of an internal type for this mapping rule item
     */
    void setMetaType(String sMetaType) {
        this.sqlType = new SqlType(sMetaType, this.sqlType);
    }

    /**
     * Assigns a Hibernate type. This type it's used in a conversion process of SQL query output values.
     * 
     * @param sHibernateType
     *            String representation of an Hibernate type for this mapping rule item
     */
    void setHibernateType(String sHibernateType) {
        this.sqlType = new SqlType(sHibernateType);
    }

    /**
     * Declares a scalar query result for this mapping rule item.
     * 
     * @param resultClass
     *            the class used for the return values, the SQL execution output
     * @param query
     *            Hibernate SQL Query instance
     */
    public void setQueryResultMapping(Class<?> resultClass, SQLQuery query) {

        Type hibernateType = null;

        if (sqlType.getMetaType() != SqlMetaType.UNKNOWN) {
            hibernateType = sqlType.getHibernateType();
        } else {
            int count = javaNames.size();
            boolean exit = false;
            Class<?> objClass = resultClass;
            for (int i = 0; i < count - 1 && !exit; i++) {
                String name = javaNames.get(i);
                Method m = SqlUtils.getMethod(objClass, SqlUtils.get(name), SqlUtils.is(name));
                if (m != null) {
                    objClass = m.getReturnType();
                } else
                    exit = true;
            }
            if (!exit) {
                String name = (count > 0) ? this.javaNames.get(count - 1) : dbName;
                hibernateType = sqlType.getHibernateType(objClass, name);
            }
        }
        if (hibernateType != null)
            query.addScalar(dbName, hibernateType);
        else
            query.addScalar(dbName);
    }

    /**
     * Initializes the attribute of the result class with output values from SQL query execution.
     * 
     * @param resultInstance
     *            the instance of the result class
     * @param resultValue
     *            query execution output value
     */
    void setQueryResultData(Object resultInstance, Object resultValue) {
        if (resultValue == null) {
            return;
        }
        int count = this.javaNames.size();
        boolean exit = false;
        Object obj = resultInstance;
        for (int i = 0; i < count - 1 && !exit; i++) {
            String name = this.javaNames.get(i);
            Method m = SqlUtils.getMethod(obj.getClass(), SqlUtils.get(name), SqlUtils.is(name));
            if (m != null) {
                Object nextObj = SqlUtils.invokeMethod(m, obj);
                if (nextObj == null) {
                    nextObj = SqlUtils.getInstance(m.getReturnType());
                    if (nextObj != null) {
                        m = SqlUtils.getMethod(obj.getClass(), SqlUtils.set(name), nextObj.getClass());
                        SqlUtils.invokeMethod(m, obj, nextObj);
                    } else
                        exit = true;
                }
                obj = nextObj;
            } else
                exit = true;
        }
        if (!exit) {
            String name = (count > 0) ? this.javaNames.get(count - 1) : this.dbName;
            this.sqlType.setResult(obj, name, resultValue);
        }
    }
}
